﻿/**
 * Peter
 * Created by: Peter Development Team
 *    http://peter.codeplex.com/
 * 
 * GNU General Public License version 2 (GPLv2)
 *    http://peter.codeplex.com/license
 *
 *  This code is provided on an AS IS basis, with no WARRANTIES,
 *  CONDITIONS or GUARANTEES of any kind.
 *
 **/

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel.Composition;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Input;
using Peter.Common;
using Peter.Common.Events;
using Peter.Common.Interfaces;
using Peter.Common.MainMenu;
using Peter.Common.Services;
using Peter.Ftp.Data;
using Peter.Ftp.DataAccess;

namespace Peter.Ftp.Logic
{
   /// <summary>
   /// Represents the main ftp model.
   /// </summary>
   public sealed class ModelFtp : PeterToolWindow, IDisposable, IPartImportsSatisfiedNotification
   {
      /// <summary>
      /// The GUID for the model ftp dock content.
      /// </summary>
      public const string GUID = "8585C3B1-7945-4930-BC34-6941A9D5C0B7";

      private OptionsFtp m_Options;
      private FtpItem m_SelectedItem;
      private RelayCommand m_NewFile;
      private RelayCommand m_NewDirectory;
      private RelayCommand m_Upload;
      private RelayCommand m_Refresh;
      private RelayCommand m_ExecuteCommand;
      private RelayCommand m_LoadConnection;
      private RelayCommand m_ManageConnections;
      private FtpConnection m_FtpConnection;
      private PeterMainMenuItem m_ConnectionMenu;

      /// <summary>
      /// Initializes a new FTP model.
      /// </summary>
      public ModelFtp ()
      {
         this.Title = "FTP";
         this.ContentId = GUID;
         this.Icon = MainMenuIcon.Ftp;
         this.FtpItems = new ObservableCollection <FtpItem> ();
         this.SetupMenu ();
      }

      /// <summary>
      /// Gets the options.
      /// </summary>
      [Import (typeof (IPeterOptions))]
      public IPeterOptions PeterOptions { get; set; }

      /// <summary>
      /// Gets the text input request service.
      /// </summary>
      [Import (typeof (ITextInputRequestService))]
      public ITextInputRequestService TextInputRequest { get; set; }

      /// <summary>
      /// Gets the password input request service.
      /// </summary>
      [Import (typeof (IPasswordRequestService))]
      public IPasswordRequestService PasswordRequest { get; set; }

      /// <summary>
      /// Gets the list of menu items for the FTP window.
      /// </summary>
      public ObservableCollection<IMainMenuHeader> MenuItems { get; private set; }

      /// <summary>
      /// Gets the list of ftp items.
      /// </summary>
      public ObservableCollection<FtpItem> FtpItems { get; private set; }

      /// <summary>
      /// Gets or Sets the selected item in the tree.
      /// </summary>
      public FtpItem SelectedItem
      {
         get { return this.m_SelectedItem; }
         set
         {
            this.m_SelectedItem = value;
            this.OnPropertyChanged ("SelectedItem");
         }
      }

      /// <summary>
      /// Gets the command to load a connection.
      /// </summary>
      public ICommand LoadConnection
      {
         get { return this.m_LoadConnection ?? (this.m_LoadConnection = new RelayCommand (this.OnLoadConnection,
            obj =>
               {
                  if (obj == null || this.m_FtpConnection == null)
                     return true;
                  var connectionData = (FtpConnectionData) obj;
                  return connectionData != this.m_FtpConnection.ConnectionData;
               })); }
      }

      /// <summary>
      /// Gets the command to manage the connections.
      /// </summary>
      public ICommand ManageConnections
      {
         get { return this.m_ManageConnections ?? (this.m_ManageConnections = new RelayCommand (this.OnManageConnection)); }
      }

      /// <summary>
      /// Gets the command to refresh the selected item.
      /// </summary>
      public ICommand Refresh
      {
         get
         {
            return this.m_Refresh ?? (this.m_Refresh = new RelayCommand (this.OnRefresh, this.CanDo));
         }
      }

      /// <summary>
      /// Gets the command to create a new file.
      /// </summary>
      public ICommand NewFile
      {
         get
         {
            return this.m_NewFile ?? (this.m_NewFile = new RelayCommand (this.OnNewFile, this.CanDo));
         }
      }

      /// <summary>
      /// Gets the command to create a new Directory.
      /// </summary>
      public ICommand NewDirectory
      {
         get
         {
            return this.m_NewDirectory ?? (this.m_NewDirectory = new RelayCommand (this.OnNewDirectory, this.CanDo));
         }
      }

      /// <summary>
      /// Gets the command to upload a new item.
      /// </summary>
      public ICommand Upload
      {
         get
         {
            return this.m_Upload ?? (this.m_Upload = new RelayCommand (this.OnUpload, this.CanDo));
         }
      }

      /// <summary>
      /// Checks to see if a command can execute or not.
      /// </summary>
      /// <param name="parameter">Command parameter, not used.</param>
      /// <returns>True if able to execute, otherwise false.</returns>
      private bool CanDo (object parameter)
      {
         return this.SelectedItem != null && (this.SelectedItem.IsDirectory || this.SelectedItem.IsRoot);
      }

      /// <summary>
      /// Gets the command used to execute an item.
      /// </summary>
      public ICommand ExecuteCommand
      {
         get
         {
            return this.m_ExecuteCommand ?? (this.m_ExecuteCommand = new RelayCommand (this.OpenSelectedItem, delegate
         {
            return this.SelectedItem != null && !this.SelectedItem.IsDirectory && !this.SelectedItem.IsRoot;
         })); }
      }

      /// <summary>
      /// Sets up the known FTP connections.
      /// </summary>
      private void SetupConnections ()
      {
         this.LoadConnections ();
         this.m_Options.FtpConnections.CollectionChanged += this.OnConnectionsChanged;
      }

      /// <summary>
      /// Opens the selected item from the tree.
      /// </summary>
      private async void OpenSelectedItem (object o)
      {
         IPeterFileDocument doc = null;
         var itemDir = this.SelectedItem.GetDirectory ();
         var file = string.Format ("Peter.FTP/{0}/{1}/{2}/{3}", this.m_FtpConnection.ConnectionData.Server,
            this.m_FtpConnection.ConnectionData.UserName, itemDir, this.SelectedItem.Text);
         var fileInfo = new FileInfo (file);
         if (fileInfo.Exists && fileInfo.Length == this.SelectedItem.Size)
         {
            // File exists and is the same size as on the server, just open...
            doc = this.Peter.Open (fileInfo.FullName);
         }
         else
         {
            // TODO: Check to see if file is already open, and is ok to overwrite...
            // Download file...
            if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
               Directory.CreateDirectory (fileInfo.Directory.FullName);
            if (await this.m_FtpConnection.ChangeWorkingDirectory (itemDir))
            {
               await this.m_FtpConnection.DownloadFile (this.SelectedItem.Text, file, false);
               doc = this.Peter.Open (fileInfo.FullName);
            }
         }
         if (doc != null) {
            doc.Saved += this.OnFileSaved;
            doc.Closed += this.OnFileClosed;
         }
      }

      /// <summary>
      /// Occurs when an item opend by FTP is saved.
      /// </summary>
      /// <param name="sender">The document saved.</param>
      /// <param name="args">The file.</param>
      private async void OnFileSaved (object sender, FileEventArgs args)
      {
         if (this.m_Options.UploadOnSave) {
            var index = args.FilePath.IndexOf ("Peter.FTP", StringComparison.Ordinal) + 10;
            var dir = args.FilePath.Substring (index, args.FilePath.LastIndexOf ('\\') - index).Replace ('\\', '/') + '/';
            dir = dir.Substring (dir.IndexOf ('/') + 1);
            dir = dir.Substring (dir.IndexOf ('/') + 1);
            dir = "/" + dir;
            await UploadFile (dir, args.FilePath);
         }
      }

      /// <summary>
      /// Occurs when an item opend by FTP is closed.
      /// </summary>
      /// <param name="sender">The document closed.</param>
      /// <param name="args">The file.</param>
      private void OnFileClosed (object sender, FileEventArgs args)
      {
         var doc = sender as IPeterFileDocument;
         if (doc != null) {
            doc.Saved -= this.OnFileSaved;
            doc.Closed -= this.OnFileClosed;
         }
      }

      /// <summary>
      /// Uploads a new file to the server.
      /// </summary>
      /// <param name="obj">Command parameter, not used.</param>
      private async void OnUpload (object obj)
      {
         var openFile = ServiceManager.Get<IOpenFileService> ();
         if (openFile != null) {
            var result = openFile.ShowDialog ();
            if (result) {
               var file = openFile.FileName;
               var itemDir = this.SelectedItem.GetDirectory ();
               if (await UploadFile (itemDir, file)) {
                  this.OnRefresh (null);
               }
            }
         }
      }

      /// <summary>
      /// Creates a new file on the server.
      /// </summary>
      /// <param name="obj">Command parameter, not used.</param>
      private async void OnNewFile (object obj)
      {
         var fileName = this.TextInputRequest.Request ("Please enter the name of the file,\n including extension.", "New File");
         if (!string.IsNullOrEmpty (fileName)) {
            var file = Path.Combine (Directory.GetCurrentDirectory (), fileName);
            var f = File.CreateText (file);
            f.WriteLine ();
            f.Flush ();
            f.Dispose();
            var itemDir = this.SelectedItem.GetDirectory ();
            if (await UploadFile (itemDir, file)) {
               File.Delete (file);
               this.OnRefresh (null);
            }
         }
      }

      /// <summary>
      /// Creates a new directory on the server.
      /// </summary>
      /// <param name="obj">Command parameter, not used.</param>
      private async void OnNewDirectory (object obj)
      {
         var dir = this.TextInputRequest.Request ("Please enter the name of the directory", "New Directory");
         if (await this.m_FtpConnection.CreateDirectory (dir)) {
            this.OnRefresh (null);
         }
      }

      /// <summary>
      /// Uploads a file.
      /// </summary>
      /// <param name="itemDir">The directory to upload to.</param>
      /// <param name="file">The file to upload.</param>
      /// <returns>The async task.</returns>
      private async Task<bool> UploadFile (string itemDir, string file)
      {
         if (await this.m_FtpConnection.ChangeWorkingDirectory (itemDir)) {
            if (await this.m_FtpConnection.UploadFile (file, false)) {
               Console.WriteLine ("Uploaded '{0}{1}' successfully!", itemDir, file.Substring (file.LastIndexOf ('\\') + 1));
               return true;
            }
         }
         Console.Error.WriteLine ("Unable to upload '{0}{1}'", itemDir, file.Substring (file.LastIndexOf ('\\') + 1));
         return false;
      }

      /// <summary>
      /// Loads the connections into the menu.
      /// </summary>
      private void LoadConnections ()
      {
         this.m_ConnectionMenu.Children.Clear ();
         foreach (var ftpConnectionData in this.m_Options.FtpConnections)
         {
            this.m_ConnectionMenu.Children.Add (new FtpConnectionMenuItem (ftpConnectionData, this.LoadConnection));
         }
         this.m_ConnectionMenu.Children.Add (new MainMenuSeperator ());
         this.m_ConnectionMenu.Children.Add (new PeterMainMenuItem
         {
            Icon = MainMenuIcon.Ftpsession,
            Title = "Manage Connections",
            ToolTip = "Opens the options to manage FTP connections",
            Command = this.ManageConnections
         });
      }

      /// <summary>
      /// Refreshes the currently selected tree item.
      /// </summary>
      /// <param name="obj"></param>
      private void OnRefresh (object obj)
      {
         this.SelectedItem.Reload ();
      }

      /// <summary>
      /// Occurs when the user requests to manage connections.
      /// </summary>
      /// <param name="obj">Command Parameter (Not Used).</param>
      private void OnManageConnection (object obj)
      {
         this.Peter.ShowOptions (this.m_Options);
      }

      /// <summary>
      /// Occurs when a connection needs to be loaded.
      /// </summary>
      /// <param name="obj">FtpConnectionData</param>
      private async void OnLoadConnection (object obj)
      {
         try
         {
            var connectionData = (FtpConnectionData) obj;
            if (connectionData != null)
            {
               // Ask for password...
               if (connectionData.AskForPassword)
               {
                  connectionData.Password = this.PasswordRequest.RequestPassword (
                     string.Format ("{2}: {0}@{1}", connectionData.UserName, connectionData.Server, connectionData.Name));
               }
               this.m_FtpConnection = new FtpConnection { DoVerbose = true };
               if (await this.m_FtpConnection.Connect (connectionData))
                  this.AddFtpItems ();
               else
               {
                  this.m_FtpConnection.Dispose();
                  this.m_FtpConnection = null;
                  Console.Error.WriteLine ("Peter.FTP - Unable to connect to the server.");
               }
            }
         }
         catch (Exception ex)
         {
            if (this.m_FtpConnection != null)
            {
               this.m_FtpConnection.Dispose ();
               this.m_FtpConnection = null;
            }
            Console.Error.WriteLine (ex.Message);
         }
      }

      /// <summary>
      /// Adds all the files from the ftp server.
      /// </summary>
      private async void AddFtpItems ()
      {
         this.FtpItems.Clear ();
         var root = new FtpItem (false, this.m_FtpConnection,
            this.m_FtpConnection.ConnectionData.Name) { IsExpanded = true };
         await this.m_FtpConnection.ChangeWorkingDirectory ("/");
         this.FtpItems.Add (root);
         root.Reload ();
      }

      /// <summary>
      /// Occurs when the ftp connections changes.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void OnConnectionsChanged (object sender, NotifyCollectionChangedEventArgs e)
      {
         this.LoadConnections ();
      }

      /// <summary>
      /// Sets up the FTP menu.
      /// </summary>
      private void SetupMenu ()
      {
         this.MenuItems = new ObservableCollection<IMainMenuHeader> { new PeterMainMenuHeader ("FTP") { IsVisible = false, IsSelected = true } };
         this.m_ConnectionMenu = new PeterMainMenuItem
                                    {
                                       Icon = MainMenuIcon.Ftpsession,
                                       ToolTip = "FTP Connections"
                                    };
         this.MenuItems[0].Children.Add (this.m_ConnectionMenu);
         this.MenuItems[0].Children.Add (new PeterMainMenuItem
         {
            Icon = MainMenuIcon.Refresh,
            ToolTip = "Refresh",
            Command = this.Refresh
         });
         this.MenuItems [0].Children.Add (new PeterMainMenuItem {
            Icon = MainMenuIcon.Upload,
            ToolTip = "Upload",
            Command = this.Upload
         });
         this.MenuItems [0].Children.Add (new PeterMainMenuItem {
            Icon = MainMenuIcon.Createfile,
            ToolTip = "New File",
            Command = this.NewFile
         });
         this.MenuItems [0].Children.Add (new PeterMainMenuItem {
            Icon = MainMenuIcon.Createfolder,
            ToolTip = "New Directory",
            Command = this.NewDirectory
         });
      }

      /// <summary>
      /// Destructor.
      /// </summary>
      ~ModelFtp ()
      {
         this.Dispose ();
      }

      /// <summary>
      /// Called when a part's imports have been satisfied and it is safe to use.
      /// </summary>
      public void OnImportsSatisfied ()
      {
         this.m_Options = (OptionsFtp) this.PeterOptions.GetOptionPage (OptionsFtp.PAGE_NAME);
         this.SetupConnections ();
      }

      /// <summary>
      /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
      /// </summary>
      /// <filterpriority>2</filterpriority>
      public void Dispose ()
      {
         Dispose (true);
         GC.SuppressFinalize (this);
      }

      /// <summary>
      /// Disposes the object.
      /// </summary>
      /// <param name="disposing">True to release managed resources.</param>
      private void Dispose (bool disposing)
      {
         if (disposing)
         {
            if (this.m_Options != null)
               this.m_Options.FtpConnections.CollectionChanged -= this.OnConnectionsChanged;
            if (this.m_FtpConnection != null)
               this.m_FtpConnection.Dispose ();
         }
      }
   }
}
